// Modified version of https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
// Handle DOM elements
// Removed global reference
// Stop traversing beyond a max depth

/*
    cycle.js
    2013-02-19

    Public Domain.

    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.

    This code should be minified before deployment.
    See http://javascript.crockford.com/jsmin.html

    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
    NOT CONTROL.
*/

/*jslint evil: true, regexp: true */

/*members $ref, apply, call, decycle, hasOwnProperty, length, prototype, push,
    retrocycle, stringify, test, toString, nodeType
*/
/* exported decycle */
'use strict';

function decycle(object, maxDepth) {
  // Make a deep copy of an object or array, assuring that there is at most
  // one instance of each object or array in the resulting structure. The
  // duplicate references (which might be forming cycles) are replaced with
  // an object of the form
  //      {$ref: PATH}
  // where the PATH is a JSONPath string that locates the first occurance.
  // So,
  //      var a = [];
  //      a[0] = a;
  //      return JSON.stringify(JSON.decycle(a));
  // produces the string '[{"$ref":"$"}]'.

  // JSONPath is used to locate the unique object. $ indicates the top level of
  // the object or array. [NUMBER] or [STRING] indicates a child member or
  // property.

  var objects = [];   // Keep a reference to each unique object or array
  var paths = [];     // Keep the path to each unique object or array

  maxDepth = maxDepth || 5;
  return (function derez(value, path, depth) {
    if (depth > maxDepth) {
      return 'Decycle max depth reached at ' + maxDepth + '. You may override the default by setting "client_decycle_depth" in your testem config.';
    }

    // The derez recurses through the object, producing the deep copy.

    var i,          // The loop counter
        name,       // Property name
        nu;         // The new object or array

    // typeof null === 'object', so go on if this value is really an object but not
    // one of the weird builtin objects.
    // Handle DOM elements
    if (value === null || typeof value === 'undefined' || value instanceof Boolean || value instanceof Number) {
      return value;
    }
    if (typeof value === 'object' && typeof value.nodeType === 'number') {
      return String(value);
    }

    if (typeof value === 'object' && value !== null &&
            !(value instanceof Date)    &&
            !(value instanceof RegExp)  &&
            !(value instanceof String)) {

      // If the value is an object or array, look to see if we have already
      // encountered it. If so, return a $ref/path object. This is a hard way,
      // linear search that will get slower as the number of unique objects grows.

      for (i = 0; i < objects.length; i += 1) {
        if (objects[i] === value) {
          return {$ref: paths[i]};
        }
      }

      // Otherwise, accumulate the unique value and its path.

      objects.push(value);
      paths.push(path);

      // If it is an array, replicate the array.

      if (Object.prototype.toString.apply(value) === '[object Array]') {
        nu = [];
        for (i = 0; i < value.length; i += 1) {
          nu[i] = derez(value[i], path + '[' + i + ']', depth + 1);
        }
      } else {

        // If it is an object, replicate the object.

        nu = {};
        for (name in value) {
          if (Object.prototype.hasOwnProperty.call(value, name)) {
            nu[name] = derez(value[name],
              path + '[' + JSON.stringify(name) + ']', depth + 1);
          }
        }
      }
      return nu;
    }
    return value;
  }(object, '$', 0));
}
